package com.shutong.edu.invoke.retry.util;

import com.shutong.edu.invoke.retry.exception.RetryException;
import com.shutong.edu.invoke.retry.model.RetryInfo;
import com.shutong.edu.invoke.retry.provider.FallbackProvider;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;

/**
 *
 * @ClassName: FallbackUtil.java
 * @author: Naughty Guo
 * @date: Jun 2, 2016
 */
public class FallbackUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(FallbackUtil.class);

    /**
     * create new parameter types.
     *
     * @param retryInfo
     * @return
     * @author Naughty Guo Jun 2, 2016
     */
    private static Class<?>[] createNewParameterTypes(RetryInfo retryInfo) {
        Class<?>[] originalParameterTypes = retryInfo.getTargetMethod().getParameterTypes();
        int length = originalParameterTypes.length;
        Class<?>[] newParameterTypes = new Class<?>[length + 2];
        for (int i = 0; i < length; i++) {
            newParameterTypes[i] = originalParameterTypes[i];
        }
        newParameterTypes[length] = Throwable.class;
        newParameterTypes[length + 1] = Object.class;
        return newParameterTypes;
    }

    /**
     * create new argument values.
     *
     * @param retryInfo
     * @return
     * @author Naughty Guo Jun 2, 2016
     */
    private static Object[] createNewArguments(RetryInfo retryInfo) {
        Object[] originalArguments = retryInfo.getArguments();
        int length = originalArguments.length;
        Object[] newArguments = new Object[length + 2];
        for (int i = 0; i < length; i++) {
            newArguments[i] = originalArguments[i];
        }
        newArguments[length] = retryInfo.getInvokeException();
        newArguments[length + 1] = retryInfo.getInvokeResult();
        return newArguments;
    }

    /**
     * execute fallback method.
     *
     * @param retryInfo
     * @return Object
     * @author Naughty Guo Jun 2, 2016
     */
    public static Object fallback(RetryInfo retryInfo) {
        if (StringUtils.isNotBlank(retryInfo.getFallbackMethod())) {
            Method targetMethod = null;
            Class<?> targetClass = retryInfo.getTargetOriginalObject().getClass();
            Class<?>[] newParameterTypes = createNewParameterTypes(retryInfo);
            try {
                targetMethod = targetClass.getDeclaredMethod(retryInfo.getFallbackMethod(), newParameterTypes);
            } catch (NoSuchMethodException e) {
                try {
                    targetMethod = targetClass.getMethod(retryInfo.getFallbackMethod(), newParameterTypes);
                } catch (NoSuchMethodException ex) {
                    LOGGER.error("Invoke fallback method error, the method is {}", retryInfo.getTargetMethodName(), ex);
                    throw new RuntimeException("Invoke fallback method error.", ex);
                }
            }
            targetMethod.setAccessible(true);
            Object[] newArguments = createNewArguments(retryInfo);
            try {
                return targetMethod.invoke(retryInfo.getTargetOriginalObject(), newArguments);
            } catch (Exception e) {
                LOGGER.error("Invoke fallback method error, the method is {}", retryInfo.getTargetMethodName(),
                        retryInfo.getInvokeException());
                throw new RuntimeException("Invoke fallback method error.", e);
            }
        }
        if (null != retryInfo.getFallbackProvider()) {
            Class<?> fallbackProviderClass = retryInfo.getFallbackProvider();
            try {
                FallbackProvider fallbackProvider = (FallbackProvider) fallbackProviderClass.newInstance();
                return fallbackProvider.fallback(retryInfo.getArguments(), retryInfo.getInvokeException(),
                        retryInfo.getInvokeResult());
            } catch (Exception e) {
                LOGGER.error("Invoke fallback provider error, the method is {}", retryInfo.getTargetMethodName(),
                        retryInfo.getInvokeException());
                throw new RuntimeException("Invoke fallback provider error.", e);
            }
        }
        throw new RetryException("Invoke fallback method error.", retryInfo.getInvokeException());
    }
}
